#Copyright (c) 2012-8 Civa Lin. All rights reserved.
"""
Web Utility 網路工具模組

本模組提供網路資料處理的功能。
"""

import urllib.request
import threading
import queue
import os.path

from . import slots

def save(url, filename, *, rename = False, timeout = 10):
    '''抓取網路資源，然後儲存成本地檔案

        指定含路徑的檔名與url，將url的檔案存到指定的檔案位置

        url      = 遠端檔案路徑
        filename = 本地檔案路徑
        rename   = 為 True 時，且路徑已經被佔用，自動更名
                   為 False 時，且路徑已經被佔用，複寫原檔案
        timeout  = 浮點數秒，表示可用的最大下載時間。
        return ==> 實際存檔路徑
        '''
    if rename:
        filename = slots.slotof_file(filename)
    with open(filename, mode = 'wb') as file:
        file.write(urllib.request.urlopen(url, timeout = 10).read())
    return filename

def saveasy(url, filename, *, rename = False, timeout = 60,
            feedback = queue.Queue(), term = False):
    '''非同步抓取一個網路上的檔案。

        除了 term 與 feedback 引數外，其餘引數含義均與 save() 相同。
        如果要確保資源抓取完畢，請對回傳引數執行 .join()。如：
            saveasync(...).join()

        term     - 是否顯示終端機輸出？
        feedback - 一個 queue 容器，內部以 dict 儲存非同步的回應訊息。
                   dict = {'about':    'websave',
                           'state':    'complete',
                           'url':      目標url,
                           'filename': 本地存檔位置}
        return ==> 負責抓圖的 thread 物件
        '''
    works = queue.Queue()
    works.put({'url': url ,'filename': filename ,
                 'rename': rename, 'timeout': timeout})
    thread = _SaveThread(works = works, feedback = feedback,
                         once = True, term = term)
    thread.start()
    return thread

#============= 物件 =============

class Dloader():
    '''非同步http網路資源下載器，可以容易且安全地使用非同步下載。

        使用示範：
            d = Dloader()
            d.save(url, filename)

        參數說明：
            works    - 為工作排程鏈，用來put入下載參數，比方說 url 位置
                       與存檔位置。
            feedback - 為回報通訊鏈，當工作完成時就會將回報訊息填入。
            threadCount - 表示背後會有多少下載線程在執行

        主要函式：
            save()   - 下載用(works.put()的封裝)
            report() - 回報訊息用(feedback.get()的封裝)
            join()   - 將會在此行等待，直到工作列表中的下載全部完成

        如果您只想少量使用幾次非同步下載，可直接用 web.saveasy()
        如果您不需要非同步功能，可以用 web.save()
        '''
    __slots__ = ()

    def __init__(self, threadCount = 4, *, works = queue.Queue(),
                 feedback = queue.Queue(), term = False):
        self.__threadCount = None
        self.__works = None
        self.__feedback = None
        self.__term = None
        self.__threads = []
        self.resetThread(threadCount = threadCount,
                         works = works,
                         feedback = feedback,
                         term = term)

    def resetThread(self, threadCount = 4, *, works = queue.Queue(),
                    feedback = queue.Queue(), term = False):
        '''放棄原本所有的下載線程，利用新的輸入參數重建一組新的。

            原本下載到一半的檔案，會在完成下載後才結束
            '''
        #終止所有現有 thread
        for thread in self.__threads:
            thread.stop()
        # 重設基礎變數
        self.__threadCount = threadCount
        self.__works = works
        self.__feedback = feedback
        self.__term = term
        self.__threads = []
        # 重新建立 self.threads
        for i in range(self.__threadCount):
            thread = _SaveThread(works = self.__works, term = self.__term,
                                 feedback = self.__feedback, once = False)
            thread.start()
            self.__threads.append(thread)

    def save(self, url, filename, *, rename = False, timeout = 60):
        '''下載指定的url資源，將該資源存放在filename的位置上'''
        #將工作放入工作槽中
        work = {'url': url ,'filename': filename,
                'rename': rename, 'timeout': timeout}
        self.__works.put(work)

    def join(self):
        '''等待所有工作完成'''
        self.__works.join()

    def report(self):
        '''立刻回報當前下載完成的項目。

            回傳為一個列表，該列表每個元素都是一個字典，字典內容包括：
                {'url': remote url, 'filename': local filename (with path)}

            每一條下載完成的訊息，都只會回報一次。
            '''
        datas = []
        try:
            while True:
                data = self.__feedback.get_nowait()
                # 去掉多餘的資料再送出……
                datas.append({'url': data['url'] , 'filename': data['filename']})
        finally:
            return datas

#================ 內部物件 ==================

class _SaveThread(threading.Thread):
    '''非同步儲存網路資源的線程物件

        初始化引數如下：
        works    = queue 容器，儲存下載資源所需的訊息。
                   每一個 item 為一個下載事件，格式為 save() 引數
                   轉成 key 的字典
                   如：{'url': url, 'filename': ...... }
        feedback = queue 容器，當下載成功時，進行回報的訊息通道。
        once     = 如果是是，則僅運行一次，確保工作完成由 Thread.join() 控制
                   如果為否，則會持續運行，工作是否完成由 works.join() 控制
        term     = 是否顯示終端機輸出
                   '''

    def __init__(self, works, feedback = None, once = True, term = False):
        '''初始化'''
        super().__init__()
        self.daemon = True
        self.__works = works
        self.__feedback = feedback
        self.__stop = once # 如果只執行一次
        self.__term = term # 終端機輸出

    def run(self):
        while True:
            try:
                # 若連續30秒沒工作可做就跳去 finally 聲明 task_done()
                data = self.__works.get(30)
                filename = save(data['url'], data['filename'],
                     rename = data['rename'], timeout = data['timeout'])
                if self.__feedback:
                    self.__feedback.put({'about': 'websave', 'state': 'complete',
                                  'url': data['url'], 'filename': filename})
                if self.__term:
                    print('DL FINISH: {}'.format(filename))
            finally: # 不管發生什麼例外，工作都要繼續完成
                self.__works.task_done()
                if self.__stop: # 如果停止旗標被指定，則在此跳出
                    return 0

    def stop(self):
        '''設定停止旗標，目前這份工作完成後就會停止，而不會繼續抓取 works 中的工作'''
        self.__stop = True
